package com.vmj.heap;

import com.vmj.classfile.entity.MemberInfo;
import com.vmj.classfile.entity.attribute.CodeAttribute;
import com.vmj.classfile.entity.attribute.ExceptionsAttribute;
import com.vmj.classfile.entity.attribute.LineNumberTableAttribute;

import java.util.ArrayList;
import java.util.List;

/**
 * Created by yangshoujun on 2017/8/26 下午1:21.
 */
public class Method extends ClassMember {
    private int maxStack;
    private int maxLocals;
    byte[] code;
    int argSlotCount;
    ExceptionsAttribute exceptions;
    MethodDescriptor parsedDescriptor;
    LineNumberTableAttribute lineNumberTable;
    protected byte[] annotationDefaultData;
    protected byte[] parameterAnnotationData;

    // 异常处理表
    ExceptionHandler[] exceptiontable;

    public static List<Method> parseMethods(VMClass clazz, List<MemberInfo> cfMethods) {

        List<Method> methods = new ArrayList<Method>();
        for(int i = 0; i < cfMethods.size();i++) {
            Method method = new Method();
            method.copyMemberInfo(cfMethods.get(i));
            method.setClazz(clazz);
            method.copyAttribute(cfMethods.get(i));
            method.argSlotCount = calculateParamCount(method);
            MethodDescriptor descriptor = new MethodDescriptorParser().parse(method.getDescriptor());
            method.parsedDescriptor = descriptor;
            if(method.isNative()) {
                // 注入native方法字节码
                injectCodeAttribute(method, descriptor.getReturnType());
            }
            methods.add(method);
        }

        return methods;
    }

    private static void injectCodeAttribute(Method method, String returnType) {
        method.maxStack = 4;
        method.maxLocals = method.argSlotCount;
        switch (returnType.charAt(0)) {
            case 'V':
                method.code = new byte[] {(byte)0xfe, (byte)0xb1};
                break;
            case 'D':
                method.code = new byte[] {(byte)0xfe, (byte)0xaf};
                break;
            case 'F':
                method.code = new byte[] {(byte)0xfe, (byte)0xae};
                break;
            case 'J':
                method.code = new byte[] {(byte)0xfe, (byte)0xad};
                break;
            case 'L':
            case '[':
                method.code = new byte[] {(byte)0xfe, (byte)0xb0};
                break;
            default:
                method.code = new byte[] {(byte)0xfe, (byte)0xac};
                break;
        }
    }

    private void copyAttribute(MemberInfo memberInfo) {
        CodeAttribute codeAttribute = memberInfo.getCodeAttribute();
        if(codeAttribute != null) {
            this.setMaxStack(codeAttribute.getMaxStack());
            this.setMaxLocals(codeAttribute.getMaxLocals());
            this.setCode(codeAttribute.getCode());
            this.exceptiontable = ExceptionHandler.getExceptionHandlers(codeAttribute.getExceptionTable(), clazz.getRuntimeConstantPool());
            this.lineNumberTable = codeAttribute.getLineNumberTableAttribute();
        }
        this.exceptions = memberInfo.getExceptionsAttribute();
        this.annotationData = memberInfo.getRuntimeVisibleAnnotationsAttributeData();
        this.parameterAnnotationData = memberInfo.getRuntimeVisibleParameterAnnotationsAttributeData();
        this.annotationDefaultData = memberInfo.getAnnotationDefaultAttributeData();
    }

    public int findExceptionHandler(VMClass exClass, int pc) {
        ExceptionHandler handler = ExceptionHandler.findExceptionHandler(exClass, pc, exceptiontable);
        if(handler != null) {
            return handler.getHandlerPc();
        }

        return -1;
    }

    public static int calculateParamCount(Method method) {
        MethodDescriptorParser parser = new MethodDescriptorParser();
        MethodDescriptor methodDescriptor = parser.parse(method.getDescriptor());
        int count = 0;
        for(String paramType : methodDescriptor.parameterTypes) {
            count++;
            if("J".equals(paramType) || "D".equals(paramType)) {
                count++;
            }
        }

        if(!method.isStatic()) {
            count++;
        }

        return count;
    }

    public int getLineNumber(int pc) {
        if(isNative()) {
            return -2;
        }

        if(lineNumberTable == null) {
            return -1;
        }

        return lineNumberTable.getLineNumber(pc);
    }

    public boolean isConstructor() {
        return !isStatic() && name.equals("<init>");
    }

    public VMClass[] getParameterTypes() {
        if(argSlotCount == 0) {
            return null;
        }

        List<String> paramTypes = parsedDescriptor.getParameterTypes();
        VMClass[] paramClasses = new VMClass[paramTypes.size()];
        for(int i = 0;i < paramClasses.length;i++) {
            String paramClassName = ClassUtils.toClassName(paramTypes.get(i));
            paramClasses[i] = clazz.getClassLoader().loadClass(paramClassName);
        }

        return paramClasses;
    }

    public VMClass[] getExceptionTypes() {
        if(exceptions == null) {
            return null;
        }

        int[] exIndexTable = exceptions.getExceptionIndexTable();
        VMClass[] exClass = new VMClass[exIndexTable.length];
        RuntimeConstantPool cp = clazz.getRuntimeConstantPool();
        for(int i = 0;i < exClass.length;i++) {
            ClassRef classRef = (ClassRef) cp.getConstant(i).getValue();
            exClass[i] = classRef.resolvedClass();
        }

        return exClass;
    }

    public byte[] getParameterAnnotationData() {
        return parameterAnnotationData;
    }

    public boolean isSynchronized() {
        return 0 != (this.accessFlags & AccessFlag.ACC_SYNCHRONIZED);
    }

    public boolean isBridge() {
        return 0 != (this.accessFlags & AccessFlag.ACC_BRIDGE);
    }

    public boolean isVarargs() {
        return 0 != (this.accessFlags & AccessFlag.ACC_VARARGS);
    }

    public boolean isNative() {
        return 0 != (this.accessFlags & AccessFlag.ACC_NATIVE);
    }

    public boolean isAbstract() {
        return 0 != (this.accessFlags & AccessFlag.ACC_ABSTRACT);
    }

    public boolean isStrict() {
        return 0 != (this.accessFlags & AccessFlag.ACC_STRICT);
    }

    public int getMaxStack() {
        return maxStack;
    }

    public int getMaxLocals() {
        return maxLocals;
    }

    public byte[] getCode() {
        return code;
    }

    public void setMaxStack(int maxStack) {
        this.maxStack = maxStack;
    }

    public void setMaxLocals(int maxLocals) {
        this.maxLocals = maxLocals;
    }

    public void setCode(byte[] code) {
        this.code = code;
    }

    public int getArgSlotCount() {
        return argSlotCount;
    }

    @Override
    public String toString() {
        return "Method{" +
                "name=" + name +
                ", clazz='" + clazz + '\'' +
                ", descriptor='" + descriptor + '\'' +
                '}';
    }
}
